```tex
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  Copyright by Wenliang Du.                                        %%
%%  This work is licensed under the Creative Commons                 %%
%%  Attribution-NonCommercial-ShareAlike 4.0 International License. %%
%%  To view a copy of this license, visit                            %%
%%  http://creativecommons.org/licenses/by-nc-sa/4.0/.               %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}


\usepackage{longtable}
\usepackage{enumitem}
\usepackage{stackengine}
\newcommand\xrowht[2][0]{\addstackgap[.5\dimexpr#2\relax]{\vphantom{#1}}}

\newcommand{\miniVPN}{{\tt MiniVPN}\xspace}
\newcommand{\hostu}{{\tt U}\xspace}
\newcommand{\hostv}{{\tt V}\xspace}

\newcommand{\vpnFigs}{./Figs}

\lhead{\bfseries SEED Labs -- VPN Lab}


\begin{document}

\begin{center}
{\LARGE 虚拟专用网络（VPN）实验室}
\end{center}

\seedlabcopyright{2006 - 2016}

% *******************************************
% SECTION
% ******************************************* 
\section{概述}

虚拟专用网络（Virtual Private Network, VPN）用于创建计算机通信的私有范围，或通过不安全的网络如互联网提供一个私人网络的安全扩展。VPN 是一种广泛使用的信息安全技术。它可以基于 IPsec 或 TLS/SSL (传输层安全性/安全套接字层) 构建。
这两种方法本质上是不同的。在本实验中，我们将重点放在基于 TLS/SSL 的 VPN 上。这种类型的 VPN 通常被称为 TLS/SSL 专用网络。

本实验室的学习目标是让学生掌握构建和实现虚拟专用网络所需的基础技术和安全原理。为了实现这一目标，学生需要实施一个简单的 TLS/SSL 虚拟专用网络（VPN）。尽管这个 VPN 简单，但包含了所有基本的组成部分。TLS/SSL 虚拟专用网络的设计与实现展示了以下几种安全原则：

\begin{itemize}[noitemsep]
    \item 虚拟专用网络
    \item TUN/TAP 和 IP 隧道化
    \item 路由
    \item 公钥加密、公钥基础设施（PKI）和 X.509 证书
    \item TLS/SSL 编程
    \item 认证
\end{itemize}

\paragraph{参考阅读和视频。}
关于虚拟专用网络 (VPN)、公钥基础设施 (PKI) 和 TLS 的详细内容可以在以下章节中找到：

\begin{itemize}
    \item SEED 书籍第 19 章、24 章和 25 章
    \item SEED 讲座第八节
\end{itemize}

\paragraph{相关实验室。}
我们有一个独立的 SEED PKI 实验室，另一个是 TLS 实验。建议学生在进行此全面的虚拟专用网络实验之前完成这两个加密实验室。如果学生仅对虚拟专用网络中的隧道部分（不涉及加密）感兴趣，应使用虚拟专用网络隧道实验室而不是这个。

\paragraph{实验环境。}
\seedenvironmentB
我们需要在这个实验中使用 \openssl 包。该包包括头文件、库和命令。
该包已经安装在我们的预构建 VM 镜像中。

\newpage

% *******************************************
% SECTION
% ******************************************* 
\section{任务}

在这个实验室中，学生需要为 Linux 实现一个简单的虚拟专用网络（VPN），我们称之为 {\tt miniVPN}。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 1：VM 设置}

我们将创建从一台计算机（客户端）到网关之间的虚拟专用网络隧道，允许该计算机通过网关安全地访问私人网络。我们需要至少三台虚拟机：VPN 客户端（也作为 Host U）、VPN 服务器（网关）和私人网络中的主机（Host V）。网络设置如图~\ref{vpn:fig:host2gateway} 所示。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.9\textwidth]{\vpnFigs/Host2Gateway.pdf}
    \caption{此实验室的 VM 设置}
    \label{vpn:fig:host2gateway}
\end{figure}

实际上，VPN 客户端和 VPN 服务器是通过互联网连接的。为了简化起见，在这个实验中我们直接将这两台机器连接到同一个局域网（LAN），即这个 LAN 模拟了互联网环境。我们将使用“NAT 网络”适配器来进行此设置。
第三台机器 Host V 是私人网络中的一个计算机。用户从外部的 Host U 想要通过 VPN 隧道与 Host V 通信，为了模拟这种设置，我们通过一个“内部网络”将 Host V 连接到作为网关的 VPN Server 上。在这种情况下，Host V 不可以直接从互联网访问；同样它也不可以直接从 Host U 访问。

注意如果 VM 使用的是“内部网络模式”，VirtualBox 将不会为其提供 DHCP 服务，因此 VM 必须进行静态配置。为此，请点击桌面上右上角的网络图标，并选择 “编辑连接”。你会看到一张显示的 “有线连接” 列表，每个网络适配器对应一个连接。对于 Host V，只有一个连接；而对 VPN Server 而言，我们将看到两个。要确保你选择了正确的与“内部网络”适配器对应的连接，请在选择连接后查看弹出窗口中显示的 MAC 地址，并将其与通过 `ifconfig` 命令获取的 MAC 地址进行比较。

在选择正确的编辑连接后，点击 “ipv4 设置” 选项卡并选择 “手动” 方法而非默认的 “自动（DHCP）”。点击 “添加” 按钮来为 VM 设置新的 IP 地址。参见图~\ref{vpn:fig:internalnetwork}。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.8\textwidth]{\vpnFigs/InternalNetwork.pdf}
    \caption{在 VPN Server 上手动设置 “内部网络”适配器的 IP 地址。}
    \label{vpn:fig:internalnetwork}
\end{figure}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 2：使用 TUN/TAP 创建虚拟专用网络隧道}

TLS/SSL 虚拟专用网络的关键技术是 TUN/TAP，现在广泛应用于现代操作系统中。
TUN 和 TAP 是虚拟网络内核驱动程序；它们实现完全在软件中的受支持的网络设备。TAP（如以太网接入点）模拟了一个以太网设备，并与第 2 层数据包（如 Ethernet 帧）一起操作；而 TUN（如网络隧道）模拟了网络层设备，它处理第 3 层数据包（如 IP 数据包）。
使用 TUN/TAP，我们可以创建虚拟网络接口。

用户空间程序通常会附加到 TUN/TAP 虚拟网络接口上。操作系统通过一个 TUN/TAP 网络接口发送的 IP 数据包会被传递给用户空间程序处理；另一方面，从程序经由一个 TUN/TAP 网络接口发送的数据包将被注入到操作系统的网络堆栈中，并对外表现为这些数据包来自虚拟网络接口外部。

当程序附加到一个 TUN/TAP 接口时，计算机发送至该接口的 IP 数据包会被管道化进入程序；另一方面，程序发送至接口的 IP 数据包会被管道化回计算机（好像它们是从虚拟网络接口外部来的）。程序可以使用标准的 `read()` 和 `write()` 系统调用来接收从虚拟接口接收到的数据包或向虚拟接口发送数据包。

我们已经创建了示例客户端程序 (`vpnclient`) 和服务器程序 (`vpnserver`)，这两个程序可以从这个实验的网站下载。
这些程序在《计算机与互联网安全：一种动手方法》一书的第 16 章中详细解释，该章节还介绍了 TUN/TAP 的工作原理以及如何使用它来创建虚拟专用网络。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.9\textwidth]{\vpnFigs/ClientServerTunnel.pdf}
    \caption{VPN 客户端和服务器。}
    \label{vpn:fig:client_server}
\end{figure}

`vpnclient` 和 `vpnserver` 是一个虚拟专用网络隧道的两个末端，它们通过图~\ref{vpn:fig:client_server} 中所示的套接字相互通信。在我们的示例代码中，我们选择使用 UDP 以简化实现。客户和服务器之间的虚线路径表示该 VPN 隧道的路径。
VPN 客户端和服务器程序连接到宿主系统通过一个 TUN 接口，通过此接口它们执行两件事情：(1) 获取来自宿主系统的 IP 数据包以便可以将这些数据包通过隧道发送；(2) 从隧道中获取 IP 数据包，并将其转发回宿主系统，宿主系统再将其转发到其最终目的地。
以下步骤描述了如何使用 `vpnclient` 和 `vpnserver` 程序创建一个虚拟专用网络隧道。

\paragraph{第 1 步：运行 VPN 服务器。}
我们首先在 Server VM 上运行 `vpnserver` 程序。程序运行后，系统会出现一个新的虚拟 TUN 网络接口（我们可以使用 `ifconfig -a` 命令查看；通常该接口的名称为 `tun0`，但也可以是 `tunX`，其中 X 是一个数字）。
这个新的接口尚未配置，因此我们需要通过给它分配 IP 地址来对其进行配置。我们将使用 `192.168.53.1` 作为此接口的 IP 地址。

运行以下命令。第一个命令将启动服务器程序，而第二个命令为 `tun0` 接口分配一个 IP 地址并激活它。需要注意的是，第一个命令将会阻塞并等待连接，因此我们需要在另一个窗口中运行第二个命令。

\begin{lstlisting}
$ sudo ./vpnserver

在另一个窗口中运行以下命令：
$ sudo ifconfig tun0 192.168.53.1/24 up
\end{lstlisting}

除非特别配置，计算机将仅作为主机工作而不会作为网关。我们需要启用 IP 转发以使该计算机像网关一样工作。IP 转发可以通过以下命令启用：

\begin{lstlisting}
$ sudo sysctl net.ipv4.ip_forward=1
\end{lstlisting}

\paragraph{第 2 步：运行 VPN 客户端。}我们现在在客户端 VM 上运行 `vpnclient` 程序。我们在这台机器上运行以下命令。第一个命令将连接到 `vpnserver` 程序（服务器的 IP 地址内嵌在程序中，你需要相应地更改它）。此命令也会阻塞，因此我们需要在另一个窗口中配置由 `vpnclient` 程序创建的 `tun0` 接口。我们将给 `tun0` 接口分配 IP 地址 `192.168.53.5`。

\begin{lstlisting}
在 Client VM 上：
$ sudo ./vpnclient 

在另一个窗口中运行以下命令
$ sudo ifconfig tun0 192.168.53.5/24 up
\end{lstlisting}

\paragraph{第 3 步：设置客户端和服务器 VM 的路由。}完成上述两步后，隧道将建立。
但在使用隧道之前，我们需要在客户端和服务器机器上设置路由路径以引导预期的流量通过隧道。在客户端机器上，我们需要将所有流向私有网络（`192.168.60.0/24`）的数据包导向 `tun0` 接口，从该接口开始数据包可以被转发通过虚拟专用网络隧道。
如果不设置此步骤，则我们将无法访问私有网络。我们可以使用 `route` 命令添加路由条目。以下示例展示了如何将针对 `10.20.30.0/24` 的数据包路由到接口 `eth0`。

\begin{lstlisting}
$ sudo route add -net 10.20.30.0/24 eth0
\end{lstlisting}

在客户端和服务器机器上，我们也需要设置一个路由条目，使所有流向 `192.168.53.0/24` 网络的数据包都被导向到 `tun0` 接口。此条目通常会自动添加，当我们给 `tun0` 接口分配 `192.168.53.X` 时。如果由于某些原因未被添加，则我们可以使用 `route` 命令来手动添加它。

\paragraph{第 4 步：设置 Host V 的路由。}当 Host V 对从 Host U 发送的包做出回应时，它需要将这些数据包路由到 VPN Server VM，在那里它们可以通过隧道传递至另一端。你需要找到要添加的条目，然后使用 `route` 命令来添加该条目。提示：当 Host V 收到来自 Host U 的数据包（通过隧道）时，你需要知道该包中的源 IP 地址；在回应包中，源 IP 地址变为目的地 IP 地址，这将用于路由表。因此，你需要弄清楚从 U 到 V 的这些包的源 IP 地址，并正确设置路由。

\paragraph{第 5 步：测试虚拟专用网络隧道。}配置完成后，我们可以通过隧道从 Host U 访问 Host V。
请使用 `ping` 和 `telnet` 进行以下测试并报告结果。你应该在客户端 VM 的所有接口上捕获网络流量，并确定哪些数据包属于隧道流量，哪些不属于。

\begin{lstlisting}
在 Host U 上：
$ ping 192.168.60.101
$ telnet 192.168.60.101
\end{lstlisting}

\paragraph{第 6 步：测试隧道中断。}在 Host U 上使用 `telnet` 连接至 `Host V`。同时保持 `telnet` 连接存活，然后我们断开虚拟专用网络隧道。然后我们在 `telnet` 窗口中输入一些内容，并报告所观察到的现象。然后重新建立隧道连接。`telnet` 连接会中断还是恢复？
请描述并解释你的观察。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 3：加密隧道}

目前，我们已经创建了一个 IP 隧道，但我们的隧道尚未得到保护。只有当我们对这个隧道进行保护后，才能称之为虚拟专用网络（VPN）隧道。
这是我们在这一步要实现的目标。为了保护此隧道，我们需要实现两个目标，即保密性和完整性。
保密性通过加密来实现，即将通过隧道的数据包加密。完整性目标确保没有人能够篡改隧道中的数据流量或发起重放攻击。使用消息认证码（MAC）可以实现这一目标。
这两个目标都可以通过传输层协议（TLS）实现。

TLS 通常建立在 TCP 之上。任务2中的示例 VPN 客户端和服务器程序使用 UDP，所以我们首先需要将示例代码中的 UDP 隧道替换为TCP隧道，并在隧道的两端之间建立一个 TLS 会话。
提供了一个包含 `tlsclient` 和 `tlsserver` 样本 TLS 客户端和服务程序的压缩文件，可以从网站上下载。包括在压缩包内的 README 文件中提供了编译和运行代码的说明。关于示例代码的详细解释，请参阅《计算机与互联网安全：一种动手方法》一书（第 25 章）。在演示过程中你需要使用 Wireshark 捕获隧道内的流量，并展示该流量确实进行了加密。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 4：验证虚拟专用网络服务器}

在建立一个虚拟专用网络之前，VPN 客户端必须验证 VPN 服务器的身份以确保这不是一个欺诈性的服务器。另一方面，VPN 服务器必须验证客户端（即用户），确保该用户有权访问私人网络。
在这个任务中，我们将实现服务端的认证；用户的认证将在下一任务中完成。

一种典型的服务器认证方式是使用公钥证书。在建立与虚拟专用网络（Virtual Private Network, VPN）服务器的连接时，服务器将使用证书来证明它就是预期中的服务器。HTTPs 协议就使用这种方法来验证 Web 服务器的身份，确保您正在与目标 Web 服务器对话而不是一个假冒者。

在这个实验中，\miniVPN 程序应使用类似的方法来验证虚拟专用网络服务器的身份。
我们可以在 scratch 从头实现一个认证协议（如 TLS/SSL），但幸运的是 OpenSSL 已经为我们处理了大部分工作。我们只需要正确地配置我们的 TLS 会话，这样 OpenSSL 就可以自动为我们进行身份验证。

服务端认证涉及三个重要步骤：（1）验证服务器证书的有效性；（2）验证服务器是否持有该证书；（3）验证服务器是否就是预期的服务器（例如，如果用户打算访问 `example.com`，我们需要确保服务器确实是 `example.com` 而不是另一个网站）。请指出程序中用于执行上述验证的具体代码行。
在演示过程中你需展示针对第三个验证的两种不同情况：一次成功的服务端身份验证（服务器确实是预期的服务器）和一个失败的服务端身份验证（服务器并不是预期的服务器）。

\paragraph{注意:} 我们的 miniVPN 程序应能够与不同机器上的虚拟专用网络服务器进行通信，因此你不能在程序中硬编码虚拟专用网络服务器的主机名。用户将通过命令行输入主机名，这代表了用户的意图，所以在验证时应该使用它。同时你也需要使用该名称来查找服务器的 IP 地址。参见~\ref{vpn:subsec:hostnametoip} 节提供的示例程序以了解如何为给定的主机名获取其 IP 地址。

\paragraph{我们提供的样本 TLS 客户端和服务器程序：} 服务端认证已由我们提供的样本程序实现。部分认证要求提供签发虚拟专用网络服务器证书的 CA 的证书。
我们在 `\./ca_client` 目录中放入了两个 CA 证书：一个是签发我们服务器证书（服务器主机名为 `vpnlabserver.com`）的 CA，另一个是签发 Google 证书的 CA。
因此我们的样本 TLS 客户端程序可以与我们自己的服务器以及 Google 的 HTTPS 服务器进行通信：

\begin{lstlisting}
$ ./tlsclient vpnlabserver.com 4433
$ ./tlsclient www.google.com 443
\end{lstlisting}

**请注意:** 学生不应使用样本代码中的 `vpnlabserver.com` 作为他们的虚拟专用网络服务器名称；相反，他们应该在其服务名中包含其姓氏。学生需要生成自己的 CA 以创建服务证书。
这一要求的目的是区分学生的作业。

为了使我们的客户端能够与 HTTPS 服务器通信，我们需要获取该服务器 CA 的证书，在 `\./ca_client` 目录下保存该证书，并使用来自其主题字段自动生成的哈希值为该证书创建符号链接（或重命名）。例如，要使我们的客户端能够与从根 CA `GeoTrust Global CA` 获取证书的 Google 通信，我们需要从 Firefox 浏览器获取这个根 CA 的证书 (`GeoTrustGlobalCA.pem`) 并运行以下命令以生成哈希值并设置符号链接：

\begin{lstlisting}
$ openssl x509 -in GeoTrustGlobalCA.pem -noout -subject_hash (*@\textbf{2c543cd1}@*)
$ ln -s GeoTrustGlobalCA.pem (*@\textbf{2c543cd1.0}@*)
$ ls -l
lrwxrwxrwx 1 ... 2c543cd1.0 -> GeoTrustGlobalCA.pem
lrwxrwxrwx 1 ... 9b58639a.0 -> cacert.pem
-rw-r--r-- 1 ... cacert.pem
-rw-r--r-- 1 ... GeoTrustGlobalCA.pem
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 5：验证虚拟专用网络客户端}

访问私人网络中的机器是授权用户才享有的特权，不是每个人都有。因此，只有授权用户才能建立与虚拟专用网络服务器的虚拟专用网络连接。
在这个任务中，授权用户是指具有有效帐户的用户。因此我们将使用标准密码认证来验证这些用户。基本上，当一个用户尝试与虚拟专用网络服务器建立虚拟专用网络连接时，系统会要求用户提供用户名和密码。服务器将检查其 `/etc/shadow` 文件；如果找到了匹配项，则该用户被认证，然后建立虚拟专用网络连接。如果没有找到匹配项，则服务器会断开与其的连接，并且不会建立隧道。

参见~\ref{vpn:subsec:auth} 节获取如何使用 `shadow` 文件验证用户的样例代码。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 6：支持多个客户端}

在现实世界中，一个虚拟专用网络服务器通常可以同时支持多个虚拟专用网络隧道。也就是说，该服务器允许超过一个客户端在同一时间连接到它，并为每个客户端提供其自己的虚拟专用网络隧道（和相应的 TLS 会话）。我们的 miniVPN 应该能够支持多个客户端。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.8\textwidth]{\vpnFigs/MultiClient.pdf}
    \caption{支持多个虚拟专用网络客户端。}
    \label{vpn:fig:MultiClient}
\end{figure}

在典型的实现中，虚拟专用网络服务器进程（父进程）将为每个隧道创建一个子进程，如图~\ref{vpn:fig:MultiClient} 所示。
当接收到从隧道来的数据包时，其对应的子进程会获取该数据包并将其转发到 TUN 接口。这种方向与是否支持多个客户端无关。但另一方向则更具挑战性。当一个数据包到达 TUN 接口（来自私有网络）时，父进程将接收该数据包，并需要决定该数据包应流向哪个隧道。你需思考如何实现这一决策逻辑。

一旦决策作出并选择了某个隧道，父进程就需要把数据包发送给附加了选定隧道的子进程。这就要求进行进程间通信（IPC）。一种常见的方法是使用管道。我们提供了一个在~\ref{vpn:subsec:pipe} 节中展示如何使用管道进行 IPC 的样例程序。

子进程需要监视这个管道接口，如果数据可用时就从该接口读取数据。由于子进程还需要同时监视来自套接字接口的数据，因此它们需要同时监控多个接口。参见~\ref{vpn:subsec:select} 节了解如何实现这一点。

% *******************************************
% SECTION
% ******************************************* 
\section{指导方针}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{在 Wireshark 中显示 TLS 通信}

Wireshark 是基于端口号识别 TLS/SSL 通信的。它知道 `443` 是 HTTPS 的默认端口，但我们的虚拟专用网络服务器监听的是一个不同的、非标准端口。
我们需要让 Wireshark 知道这一点；否则，Wireshark 将不会将我们的流量标记为 SSL/TLS 流量。我们可以这样做：在 Wireshark 中点击 `Edit` 菜单，然后点击 `Preferences -> Protocols -> HTTP -> "SSL/TLS Ports"` 条目。添加你的 SSL 服务器端口。例如，我们可以将该条目的内容更改为 `443,4433`，其中 `4433` 是我们的 SSL 服务器所使用的端口号。

\paragraph{显示解密的数据流。} 上述方法仅让 Wireshark 认识到此通信为 TLS/SSL 通信；Wireshark 无法解密加密的流量。为了进行调试，我们需要看到解密后的数据流。
Wireshark 提供了这种功能；我们只需提供服务器的私钥给 Wireshark，它将自动从 TLS/SSL 握手协议中推导出会话密钥并使用这些密钥来解密流量。要向 Wireshark 提供服务器的私钥，请执行以下操作：

\begin{lstlisting}
点击 Edit -> Preferences -> Protocols -> SSL
找到 "RSA key list"，然后点击编辑按钮
提供有关服务器所需的信息，例如：
      IP 地址: 10.0.2.65
      端口:       4433
      协议:   ssl
      密钥文件: /home/seed/vpn/server-key.pem (私钥文件)
      密码:   deesdees
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{通过主机名获取 IP 地址}
\label{vpn:subsec:hostnametoip}

给定一个主机名，我们可以获得这个名称对应的 IP 地址。在我们的示例 `tlsclient` 程序中，我们使用了 `gethostbyname()` 函数来获取 IP 地址。然而，该函数已过时，因为它不支持 IPv6。
应用程序应使用 `getaddrinfo()` 代替。以下示例展示了如何使用此功能来获取 IP 地址。

\begin{lstlisting}
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

struct addrinfo hints, *result;

int main() {

  hints.ai_family = AF_INET; // AF_INET 表示仅限 IPv4 地址

  int error = getaddrinfo("www.example.com", NULL, &hints, &result);
  if (error) {
      fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error));
      exit(1);
  }

  // 结果可能包含一系列 IP 地址；我们只取第一个。
  struct sockaddr_in* ip = (struct sockaddr_in *) result->ai_addr;
  printf("IP Address: %s\n", (char *)inet_ntoa(ip->sin_addr));

  freeaddrinfo(result);
  return 0;
}
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{使用阴影文件进行认证}
\label{vpn:subsec:auth}

以下程序展示了如何使用存储在 shadow 文件中的帐户信息来认证用户。
该程序使用 `getspnam()` 从 shadow 文件中获取给定用户的帐户信息，包括哈希后的密码。然后它使用 `crypt()` 将给定的密码进行哈希，并与从 shadow 文件检索到的信息进行比较。如果匹配，则用户名和密码匹配且身份验证成功。

\begin{lstlisting}
#include <stdio.h>
#include <string.h>
#include <shadow.h>
#include <crypt.h>

int login(char *user, char *passwd)
{
    struct spwd *pw;
    char *epasswd;

    pw = getspnam(user);
    if (pw == NULL) {
        return -1;
    }

    printf("Login name: %s\n", pw->sp_namp);
    printf("Passwd    : %s\n", pw->sp_pwdp);

    epasswd = crypt(passwd, pw->sp_pwdp);
    if (strcmp(epasswd, pw->sp_pwdp)) {
        return -1;
    }

    return 1;
}

void main(int argc, char** argv)
{
   if (argc < 3) {
       printf("Please provide a user name and a password\n");
       return;
   }

   int r = login(argv[1], argv[2]);
   printf("Result: %d\n", r);
}
\end{lstlisting}

我们可以编译上述代码并使用用户名和密码运行它。请注意，当读取 shadow 文件时需要 root 权限。以下命令用于编译和执行：

\begin{lstlisting}
$ gcc login.c -lcrypt
$ sudo ./a.out seed dees
\end{lstlisting}

请注意我们在这里使用了 `-lcrypt`；我们在编译我们的 TLS 程序时使用的是 `-lcrypto`。`crypt` 和 `crypto` 是两个不同的库，因此这不是笔误。

\vspace{0.2in}
% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{通过管道进行进程间通信}
\label{vpn:subsec:pipe}

以下程序展示了如何使用 pipe 从父进程向其子进程发送数据。父进程在第 \ding{192} 行使用 `pipe()` 函数创建了一个 pipe。每个 pipe 都有两个端点：输入端的文件描述符为 `fd[0]`，输出端的文件描述符为 `fd[1]`。

创建 pipe 后，通过调用 `fork()` 创建一个子进程。父进程和子进程都具有与管道关联的文件描述符。它们可以使用管道相互发送数据，这是一个双向的过程。然而，我们只会从父进程向子进程发送数据，因此在父进程中关闭输入端 `fd[0]`。类似地，在子过程中不通过管道发送任何内容，因此它会关闭输出端 `fd[1]`。
此时已经建立了从父进程到子进程的单向管道。要通过管道发送数据，父进程将写入 `fd[1]`（见第 \ding{193} 行）；而子进程则从 `fd[0]` 读取数据（见第 \ding{194} 行）。


\begin{lstlisting}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd[2], nbytes;
    pid_t pid;
    char string[] = "Hello, world!\n";
    char readbuffer[80];

    pipe(fd);                                                  (*@\ding{192}@*)
        
    if((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }

    if(pid > 0) { //父进程 
        close(fd[0]); // 关闭管道输入端的文件描述符。 

        // 向管道写入数据。
        write(fd[1], string, (strlen(string)+1));             (*@\ding{193}@*)
        exit(0);
    }
    else { //子进程
        close(fd[1]); // 关闭管道输出端的文件描述符。

        // 从管道读取数据。
        nbytes = read(fd[0], readbuffer, sizeof(readbuffer)); (*@\ding{194}@*)
        printf("子进程接收到字符串：%s", readbuffer);
    }
    return(0);
}
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{使用 `select` 监控多个输入接口}
\label{vpn:subsec:select}

我们的虚拟专用网络程序需要监控包括 TUN 接口、套接字接口以及有时还有管道接口在内的多个接口。  
所有这些接口都表示为文件描述符，因此我们需要监视它们以查看是否有数据从它们传来。
一种方法是不断轮询它们并检查每个接口上是否有数据。这种方法的性能不佳，因为在没有任何数据时进程不得不在一个空闲循环中保持运行状态。
另一种方法是从一个接口读取。默认情况下，`read` 是阻塞操作；如果当前没有数据，则进程将被挂起。 当有数据可读时，该过程会被取消阻塞并继续执行。
这种方法在没有数据时不会浪费 CPU 时间。

基于 `read` 的阻塞机制对单个接口工作得很好。 如果一个进程需要等待多个接口的数据，则它不能在一个接口上阻塞，而是必须同时阻塞所有这些接口。 Linux 有一个名为 `select()` 的系统调用，该调用允许程序同时监视多个文件描述符。
为了使用 `select()`, 我们需要将要监控的文件描述符存储到一个集里（见代码中第 \ding{192} 和 \ding{193} 行）。然后我们将这个集合传递给 `select()` 系统调用（见代码中的第 \ding{194} 行），该调用会使进程阻塞，直到集合中某个文件描述符上有数据。我们可以通过 `FD_ISSET` 宏来确定哪个文件描述符接收到数据。
以下代码示例展示了如何使用 `select()` 监控 TUN 接口和套接字的文件描述符。

\begin{lstlisting}
fd_set readFDSet;
int ret, sockfd, tunfd;

FD_ZERO(&readFDSet);
FD_SET(sockfd, &readFDSet);                                (*@\ding{192}@*)
FD_SET(tunfd, &readFDSet);                                 (*@\ding{193}@*)
ret = select(FD_SETSIZE, &readFDSet, NULL, NULL, NULL);    (*@\ding{194}@*)

if (FD_ISSET(sockfd, &readFDSet){
        // 从 sockfd 读取数据并做些事情。
}

if (FD_ISSET(tunfd, &readFDSet){
        // 从 tunfd 读取数据并做些事情。 
}
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{一个示例：在虚拟专用网络中使用 `telnet`} 

为了帮助你更全面地理解应用程序的数据包如何通过我们的 miniVPN 流向其目的地，我们绘制了两张图以展示当用户从主机机器（点 A）运行 `telnet 10.0.20.100` 命令时的完整数据流路径。另一端的虚拟专用网络在网关上，并连接到包含 telnet 服务器 10.0.20.100 的 10.0.20.0/24 网络。

图~\ref{fig:example1_host_2_gateway} 显示了从 `telnet` 客户端发送数据包到服务端的过程。 图~\ref{fig:example2_host_2_gateway} 显示了从 `telnet` 服务器回送到客户端的路径。
在以下内容中，我们将仅描述图~\ref{fig:example1_host_2_gateway} 中的数据流路径。返回路径可以在你理解图~\ref{fig:example1_host_2_gateway} 的路径后自行解释。

1. 数据包的内容开始于 `telnet` 程序。
2. 内核将构建一个 IP 数据包，目标 IP 地址为 `10.0.20.100`。
3. 内核需要决定数据包应通过哪个网络接口路由：`eth1` 或 `tun0`。你需要正确地设置你的路由表，以便内核可以选取 `tun0` 作为路径。一旦作出决策后，内核将使用与所选网络接口对应的 IP 地址（这里是 `10.0.4.1`）设置数据包的源 IP 地址。

4. 数据包会通过虚拟网络接口 `tun0` 到达我们的 VPN 程序（点 A），然后被加密，并通过 UDP 端口重新发送回内核。这是因为我们的虚拟专用网络程序使用 UDP 作为隧道。
5. 内核将把加密的 IP 数据包视为 UDP 数据，构造一个新的 IP 数据包，其 UDP 载荷为整个加密的 IP 数据包。新数据包的目标地址是隧道另一端（由我们编写的程序决定）；在图中，新数据包的目标地址为 `128.230.208.97`。
6. 你需要正确地设置路由表，使新数据包经过接口 `eth1` 路由，因此该新数据包的源 IP 地址应为 `209.164.131.32`。

7. 数据包现在将通过互联网流动，原始的 `telnet` 数据包被完全加密，并承载在数据包的有效载荷中。这就是为什么称之为一个 {\em 隧道} 的原因。
8. 数据包将通过网关 `128.230.208.97` 从其接口 `eth1` 到达。
9. 内核会将 UDP 载荷（即加密的 IP 数据包）交给正在等待 UDP 数据的 VPN 程序（点 B），这是通过 UDP 端口实现的。
10. VPN 程序将解密载荷，并向内核通过虚拟网络接口 `tun0` 发送解密后的数据包，即原始 `telnet` 数据包。
11. 由于它经过了网络接口，因此内核会将其视为一个 IP 数据包（确实是一个），查看其目标 IP 地址并决定如何路由。请记住，该数据包的目标 IP 地址为 `10.0.20.100`。如果路由表设置正确，则该数据包应通过 `eth2` 接口被路由，因为它连接到 `10.0.20.0/24` 网络。
12. 最终，`telnet` 数据包将被递送到其最终目的地：`10.0.20.100`。

\begin{figure*}
    \centering
    \subfigure[主机到网关隧道中的 Telnet 客户端到服务器的数据流示例]
    {
       \label{fig:example1_host_2_gateway}
       \includegraphics*[viewport=0.7in 1.7in 8.7in 7.3in,width=6.0in]{\vpnFigs/vpn_h2g_details.pdf}
    }
    \\
    \subfigure[主机到网关隧道中的 Telnet 服务器到客户端的数据流示例]
    {
       \label{fig:example2_host_2_gateway}
       \includegraphics*[viewport=1.6in 1.6in 9.6in 7.2in,width=6.0in]{\vpnFigs/vpn_h2g_details_reply.pdf}
    }
    \caption{虚拟专用网络中的数据流示例。}
    \label{fig:example_packetflow}
\end{figure*}

% *******************************************
% SECTION
% ******************************************* 
\section{提交和演示}
\input{demo}

% *******************************************
% SECTION
% ******************************************* 
\section{演示清单}

在疫情期间，我们无法进行面对面的演示。虽然可以在线进行演示，但我们决定尝试一个不同的方法：要求学生录制他们的演示并提交视频文件。
为了帮助他们自己指导演示过程，我们提供了一个如表~\ref{vpn:table:checklist} 所示的清单。即使将来我们进行面对面的演示，这个清单也非常有用。

\input{Demo_Checklist}

\end{document}
```